import scala.xml.{NodeSeq, Text}
import net.liftweb._
import util._
import common._
import http.js.JsCmds.{Alert, Noop, FocusOnLoad, RedirectTo}
import http.js.JE._
import http.{S, SHtml}
import java.util.Date
import com.commutersconnect.lib._
import Helpers._

import com.commutersconnect.model.{Report, User, Img}

package com.commutersconnect.snippet {
  //A new report has a small twist.  When the snippet is begun, we create the
  //Report object and throw it in a map so that the ajax upload can find it.
  //(TODO I have not looked up concurrent considerations of mutable.Map.)
  object NewReport extends Logger {

    val unsavedReports:collection.mutable.Map[String, Report] = collection.mutable.Map()

    def unsavedReport(id: String) = unsavedReports.synchronized { unsavedReports.get(id) }

    def render = {
      import java.util.UUID.randomUUID

      //We do require that a user be logged in.
      User.currentUser.map( curUser => {
        val report = Report.create.reporter(curUser)
        val uuid = randomUUID.toString
        unsavedReports.synchronized {
          unsavedReports += (uuid -> report)
        }


        "@ajaxUploadId [value]" #> uuid &
        "@subject" #> FocusOnLoad(SHtml.text(report.subject, report.subject(_))) &
        "@location" #> SHtml.ajaxText("", location => {
          //We do have to null out the values for lat/long in case you change your
          //mind.
          report.latitude(0).longitude(0)
          if (location != "") {
            geocode(location) match {
              case Some((lat, lng)) =>
                report.latitude(lat).longitude(lng)
                Call("CommutersConnectNewReport.showLocation", lat, lng).cmd
              case None =>
                debug("No results found in geocode.")
                Noop
            }
          }
        }) &
        "@text" #> SHtml.textarea(report.text, report.text(_)) &
        "@create" #> SHtml.ajaxSubmit("Create", () => {
          report.validate match {
            case Nil =>
              report.save
              unsavedReports.synchronized {
                unsavedReports.remove(uuid)
              }
              RedirectTo("/")
            case errors =>
              S.error(errors)
              Noop
            }
        })
      }) openOr "*" #> "You must be logged in."
    }

    /**
    * Runs a geocode lookup over to the Google Map API.  Returns the top hit as
    * a lat/lng tuple.
    */
    def geocode(location:String) = {
      import java.net.URL
      import java.net.URLEncoder.encode
      val url = new URL(
        "http://maps.googleapis.com/maps/api/geocode/xml?address=%s&sensor=false"
        .format(encode(location+", Atlanta, GA", "utf-8"))
      )
      val geocodedXml = xml.XML.load(url)
      geocodedXml \\ "result" toList match {
        case head :: tail => {
          val locNode = head \\ "location"
          Some(((locNode \ "lat").text.toDouble, (locNode \ "lng").text.toDouble))
        }
        case Nil => None
      }
    }
  }
}

package com.commutersconnect.rest {
  /**
   * Provides the facilities to handle file uploads from the user on new
   * reports.  This is closely coordinated with the NewReport snippet.
   *
   * At the moment this is copied shamelessly from an example put together by
   * dpp.  https://github.com/dpp/starting_point/tree/ajax_upload
   */
  object NewFileUpload extends http.rest.RestHelper with Logger {
    //We use a few more imports throughout this object.  In particular, a lot
    //more of the details for setting up the REST and building Json are
    //necessary.
    import json._
    import json.JsonDSL._
    import http.{LiftRules, RewriteResponse, RewriteRequest, ParsePath,
  JsonResponse, InMemoryResponse, NotFoundResponse}
    import com.commutersconnect.snippet.NewReport

    //We will accept a request to newReport/attachImage/uuid.  We start by
    //looking up the report instance that this is associated with, and if found,
    //we create a new Img instance and associate it with the given report.
    serve {
      case "newReport" :: "attachImage" :: id :: Nil Post req => {
        val uploads = req.uploadedFiles

        //You must be logged in to attach a file to a report.  Likely overkill
        //to check both this and require a unique key, but we go ahead and make
        //sure anyway. (Granted, amusing to talk about overkill when I'm not yet
        //cleaning up reports that aren't saved. :()
        if (User.loggedIn_?) {
          NewReport.unsavedReport(id) match {
            case Some(report) => {
              val ojv: List[JObject] = uploads.map(fph => {
                val img = Img.create.data(fph.file).report(report)
                report.images += img

                val metadata = getMetadata(img.data)

                //Since we don't get the location in the ajax call if it is
                //set by javascript, we need to ensure that the report has the
                //lat/lng set here.  (Or do we??)
                metadata._2.map(lat=>metadata._3.map(lng=>{
                    report.latitude(lat).longitude(lng)
                  }))

                //Now, we prepare the response that the JQuery FileUpload plugin is
                //expecting.
                ("name" -> fph.fileName) ~
                  ("sizef" -> fph.length) ~
                  ("url" -> "/report/%s/image".format(report.id.toString)) ~
                  ("dateTaken" -> (metadata._1 getOrElse "")) ~
                  ("lat" -> (metadata._2 getOrElse 0d)) ~
                  ("lng" -> (metadata._3 getOrElse 0d)) ~
                  ("location" -> (reverseGeocode(metadata._2, metadata._3) getOrElse ""))
              })

                // This is a tad bit of a hack, but we need to return text/plain, not JSON
                val jr = JsonResponse(ojv).toResponse.asInstanceOf[InMemoryResponse]

                InMemoryResponse(jr.data, ("Content-Length", jr.data.length.toString) ::
                  ("Content-Type", "text/plain") :: S.getHeaders(Nil),
                  S.responseCookies, 200)
            }
            case None =>
              NotFoundResponse()
          }
        } else {
          NotFoundResponse()
        }

      }

    }

    //We are using the metadata extraction code from
    //http://code.google.com/p/metadata-extractor/ to get some information from
    //the images.  The data we are looking up from the image is the Date it was
    //taken, and if it has a lat/lng attached.
    def getMetadata(img:Array[Byte]) = {
      import com.drew.metadata.exif.ExifDirectory
      import com.drew.metadata.exif.GpsDirectory
      import com.drew.imaging.ImageMetadataReader.readMetadata
      import java.io.ByteArrayInputStream
      import java.io.BufferedInputStream

      //We can not guarantee that this library can return the appropriate
      //information.  As such, we will let it try, but will return all blanks if
      //it fails.
      try {
        val metadata = readMetadata(new BufferedInputStream(new ByteArrayInputStream(img)))

        val date = try {
          val exifData = metadata.getDirectory(classOf[ExifDirectory])
          Some(exifData.getString(ExifDirectory.TAG_DATETIME_ORIGINAL))
        } catch { case _ => None }

        val (lat, lng) = try {
          //We will need to convert from the rational array that
          //metadata-extractor uses to decimal degrees.  The equation is straight
          //forward changing from degrees minutes seconds to decimal degrees.
          import com.drew.lang.Rational
          implicit def rationalArrayToDouble(in:Array[Rational]):Double = {
            in zip (List(1, 60, 3600)) map (x => x._1.doubleValue/x._2) sum
          }

          val gpsData = metadata.getDirectory(classOf[GpsDirectory])
          var lat = gpsData.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE):Double
          var lng = gpsData.getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE):Double
          if (gpsData.getString(GpsDirectory.TAG_GPS_LATITUDE_REF) == "S") {
            lat *= -1
          }
          if (gpsData.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF) == "W") {
            lng *= -1
          }

          (Some(lat), Some(lng))
        } catch { case _ => (None, None) }

        (date, lat, lng)
      } catch { case _ => (None, None, None) }
    }

    /**
     * As in geocode in the view snippet, we are using the google maps api to
     * lookup this information.  We take an optional lat and long and return the
     * location information for it. As before, we return the top result.  We do
     * run a quick removal of any trailing ", Atlanta, GA.."
     */
    def reverseGeocode(lat: Option[Double], lng: Option[Double]):Option[String] = {
      import java.net.URL
      lat.flatMap(lat=> lng.flatMap(lng => {
        val url = new URL(
            "http://maps.googleapis.com/maps/api/geocode/xml?latlng=%s,%s&sensor=false"
            .format(lat, lng)
        )
        val geocodedXml = xml.XML.load(url)
        geocodedXml \\ "result" toList match {
          case head :: tail => {
            val address = (head \\ "formatted_address").text.replaceAll(", Atlanta, GA.*", "")
            Some(address)
          }
          case Nil => None
        }
      }))
    }
    def init() = {
      //rewrite so the rest-callback will be a param instead to be fired with LiftSession.runParams
      LiftRules.statelessRewrite.append {
        case RewriteRequest(ParsePath("report" :: id :: "attachImage" :: callback :: Nil, "", true, _), _, _) =>
          RewriteResponse("report" :: id :: "attachImage" :: Nil, Map("callback" -> "_"))
      }

      LiftRules.dispatch.append(this)
    }
  }
}
